package tw.com.hitevision.whiteboard.android.render;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.Typeface;
import android.text.TextPaint;
import android.util.Log;

import androidx.annotation.ColorInt;
import androidx.core.content.ContextCompat;

import java.util.Locale;

import tw.com.hitevision.whiteboard.android.R;
import tw.com.hitevision.whiteboard.android.state.HighlighterProperty;
import tw.com.hitevision.whiteboard.android.state.LineProperty;
import tw.com.hitevision.whiteboard.android.state.PenProperty;
import tw.com.hitevision.whiteboard.android.stateFile.BaseStateFile;
import tw.com.hitevision.whiteboard.android.stateFile.ProtractorItemStateFile;
import tw.com.hitevision.whiteboard.android.util.TextLayoutUtil;
import tw.com.hitevision.whiteboard.android.widgets.MeasurementTools.ProtractorPaintMode;

public class ProtractorItem extends BaseItem {
    private PointF centerP;
    private PointF startP;
    private float protractorDegree;
    private Paint protractorPaint;
    private Path protractorPath;
    private Path detectPath;
    private float padding;
    private boolean update;
    private TextPaint textPaint;
    private Paint textRectFPaint;
    private float textRadius;
    private PointF textP;
    private String text;
    private RectF textRectF;
    private float textBackgroundHorizontalPadding;
    private float textBackgroundVerticalPadding;
    private Region detectRegion;
    private Context context;
    private ProtractorPaintMode mode;
    private boolean showDegree = true;
    private float measurementRectRadius;

    @ColorInt
    private int hlBlack;
    @ColorInt
    private int hlWhite;
    @ColorInt
    private int hlRed;
    @ColorInt
    private int hlYellow;
    @ColorInt
    private int hlBlue;
    @ColorInt
    private int hlGreen;
    @ColorInt
    private int hlPurple;

    @ColorInt
    private int black;
    @ColorInt
    private int white;
    @ColorInt
    private int red;
    @ColorInt
    private int yellow;
    @ColorInt
    private int green;
    @ColorInt
    private int blue;
    @ColorInt
    private int purple;

    private Paint debugPaint;

    public ProtractorItem(Context context, LineProperty lineProperty, float scale, PointF centerP,
                          PointF startP, float degree, ProtractorPaintMode mode) {
        @ColorInt int strokeColor = 0xffffff;
        float strokeWidth = 1;
        if (lineProperty instanceof PenProperty) {
            strokeWidth = ((PenProperty) lineProperty).getStrokeWidth() * scale;
            strokeColor = ((PenProperty) lineProperty).getColor();
        } else if (lineProperty instanceof HighlighterProperty) {
            strokeWidth = ((HighlighterProperty) lineProperty).getStrokeWidth() * scale;
            strokeColor = ((HighlighterProperty) lineProperty).getColor();
        }
        init(context, strokeColor, strokeWidth, centerP, startP, degree, mode);
    }

    public ProtractorItem(Context context, @ColorInt int strokeColor, float strokeWidth, PointF centerP,
                          PointF startP, float degree, ProtractorPaintMode mode) {
        init(context, strokeColor, strokeWidth, centerP, startP, degree, mode);
    }

    private void init(Context context, @ColorInt int strokeColor, float strokeWidth, PointF centerP,
                      PointF startP, float degree, ProtractorPaintMode mode) {
        this.context = context;
        protractorPaint = new Paint();
        protractorPaint.setStrokeWidth(strokeWidth);
        protractorPaint.setColor(strokeColor);
        protractorPaint.setStyle(Paint.Style.STROKE);
        protractorPaint.setAntiAlias(true);
        protractorPaint.setStrokeJoin(Paint.Join.ROUND);
        protractorPaint.setStrokeCap(Paint.Cap.ROUND);
        this.centerP = new PointF(centerP.x, centerP.y);
        this.startP = new PointF(startP.x, startP.y);
        this.protractorDegree = degree;
        protractorPath = new Path();
        detectPath = new Path();
        padding = context.getResources().getDimensionPixelOffset(R.dimen.ruler_protractor_item_selection_padding);
        textPaint = new TextPaint();
        textPaint.setColor(getTextPaintColor(protractorPaint.getColor(), mode));
        textPaint.setTextSize(context.getResources().getDimensionPixelOffset(R.dimen.ruler_protractor_item_text_size));
        textPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
        textPaint.setAntiAlias(true);
        textPaint.setColor(Color.WHITE);
        textRadius = context.getResources().getDimensionPixelOffset(R.dimen.ruler_protractor_item_text_radius);
        detectRegion = new Region();
        textRectF = new RectF();
        textRectFPaint = new Paint();
        textRectFPaint.setColor(context.getResources().getColor(R.color.ruler_protractor_degree_background));
        textRectFPaint.setAntiAlias(true);
        measurementRectRadius = context.getResources().getDimensionPixelOffset(R.dimen.ruler_protractor_degree_background_radius);
        textBackgroundHorizontalPadding = context.getResources().getDimensionPixelOffset(R.dimen.ruler_protractor_degree_background_horizontal_padding);
        textBackgroundVerticalPadding = context.getResources().getDimensionPixelOffset(R.dimen.ruler_protractor_degree_background_vertical_padding);
        this.mode = mode;
        this.setColor(strokeColor);

        hlBlack = ContextCompat.getColor(context, R.color.highlight_black);
        hlWhite = ContextCompat.getColor(context, R.color.highlight_white);
        hlRed = ContextCompat.getColor(context, R.color.highlight_red);
        hlYellow = ContextCompat.getColor(context, R.color.highlight_yellow);
        hlBlue = ContextCompat.getColor(context, R.color.highlight_blue);
        hlGreen = ContextCompat.getColor(context, R.color.highlight_green);
        hlPurple = ContextCompat.getColor(context, R.color.highlight_purple);

        black = ContextCompat.getColor(context, R.color.pen_black);
        white = ContextCompat.getColor(context, R.color.pen_white);
        red = ContextCompat.getColor(context, R.color.pen_red);
        yellow = ContextCompat.getColor(context, R.color.pen_yellow);
        green = ContextCompat.getColor(context, R.color.pen_green);
        blue = ContextCompat.getColor(context, R.color.pen_blue);
        purple = ContextCompat.getColor(context, R.color.pen_purple);

        update = true;

        debugPaint = new Paint();
        debugPaint.setStrokeWidth(2);
        debugPaint.setColor(context.getResources().getColor(R.color.hitevision_blue));
        debugPaint.setStyle(Paint.Style.STROKE);
        debugPaint.setAntiAlias(true);
        debugPaint.setStrokeJoin(Paint.Join.ROUND);
        debugPaint.setStrokeCap(Paint.Cap.ROUND);
    }

    private @ColorInt
    int getTextPaintColor(@ColorInt int color, ProtractorPaintMode mode) {
        if (mode == ProtractorPaintMode.SOLID) {
            return Color.argb(Color.alpha(color), 255 - Color.red(color), 255 - Color.green(color), 255 - Color.blue(color));
        } else {
            return color;
        }
    }

    public float getProtractorDegree() {
        return protractorDegree;
    }

    private void updatePath() {
        float radius = PointF.length(startP.x - centerP.x, startP.y - centerP.y);
        float degree = protractorDegree;
        if (mode != ProtractorPaintMode.ANGLE) {
            if (degree >= 360) {
                degree -= 0.001f;
            } else if (degree <= -360) {
                degree += 0.001f;
            }
        }

        protractorPath.rewind();
        switch (mode) {
            case ARC:{
                protractorPath.moveTo(startP.x, startP.y);
                protractorPath.addArc(centerP.x - radius, centerP.y - radius,
                        centerP.x + radius, centerP.y + radius,
                        getStartDegree(startP.x, startP.y, centerP.x, centerP.y), degree);
                break;
            }
            case ANGLE:{
                protractorPath.moveTo(startP.x, startP.y);
                protractorPath.lineTo(centerP.x, centerP.y);
                PointF endP = new PointF(startP.x, startP.y);
                Matrix matrix = new Matrix();
                matrix.setRotate(degree, centerP.x, centerP.y);
                float[] points = new float[]{endP.x, endP.y};
                matrix.mapPoints(points);
                endP.set(points[0], points[1]);
                protractorPath.lineTo(endP.x, endP.y);
                break;
            }
            case LINE:
            case SOLID: {
                protractorPath.moveTo(centerP.x, centerP.y);
                protractorPath.lineTo(startP.x, startP.y);
                protractorPath.addArc(centerP.x - radius, centerP.y - radius,
                        centerP.x + radius, centerP.y + radius,
                        getStartDegree(startP.x, startP.y, centerP.x, centerP.y), degree);
                if (Math.abs(degree) < 360) {
                    protractorPath.lineTo(centerP.x, centerP.y);
                }
                break;
            }
        }

        if (mode == ProtractorPaintMode.SOLID) {
            protractorPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        } else {
            protractorPaint.setStyle(Paint.Style.STROKE);
        }
    }

    private void updateDetectPath() {
        float degree = protractorDegree;
        if (mode != ProtractorPaintMode.ANGLE) {
            if (degree >= 360) {
                degree -= 0.001f;
            } else if (degree <= -360) {
                degree += 0.001f;
            }
        }
        PointF endP = new PointF(startP.x, startP.y);
        Matrix matrix = new Matrix();
        matrix.setRotate(degree, centerP.x, centerP.y);
        float[] points = new float[]{endP.x, endP.y};
        matrix.mapPoints(points);
        endP.set(points[0], points[1]);

        float w = protractorPaint.getStrokeWidth() / 2f;
        int startOut = degree > 0 ? 1 : -1;
        int endOut = degree > 0 ? -1 : 1;

        float radius = PointF.length(startP.x - centerP.x, startP.y - centerP.y) + w;
        float interRadius = PointF.length(startP.x - centerP.x, startP.y - centerP.y) - w;
        PointF cStartV = getUnitVector(startP.x - centerP.x, startP.y - centerP.y);
        PointF cEndV = getUnitVector(endP.x - centerP.x, endP.y - centerP.y);
        PointF cStartNv = new PointF(startOut * cStartV.y, -startOut * cStartV.x);
        PointF cEndNv = new PointF(endOut * cEndV.y, -endOut * cEndV.x);
        PointF startPmoveV = new PointF(startP.x + cStartV.x * w, startP.y + cStartV.y * w);
        PointF startPmoveNv = new PointF(startP.x + cStartNv.x * w, startP.y + cStartNv.y * w);
        PointF startPmoveValt = new PointF(startP.x - cStartV.x * w, startP.y - cStartV.y * w);
        PointF startPmoveNvalt = new PointF(startP.x - cStartNv.x * w, startP.y - cStartNv.y * w);
        PointF endPmoveV = new PointF(endP.x + cEndV.x * w, endP.y + cEndV.y * w);
        PointF endPmoveNv = new PointF(endP.x + cEndNv.x * w, endP.y + cEndNv.y * w);
        PointF endPmoveValt = new PointF(endP.x - cEndV.x * w, endP.y - cEndV.y * w);
        PointF endPmoveNvalt = new PointF(endP.x - cEndNv.x * w, endP.y - cEndNv.y * w);
        PointF centerPmoveStartV = new PointF(centerP.x + cStartNv.x * w, centerP.y + cStartNv.y * w);
        PointF centerPmoveEndV = new PointF(centerP.x + cEndNv.x * w, centerP.y + cEndNv.y * w);
        PointF centerPmoveStartValt = new PointF(centerP.x - cStartNv.x * w, centerP.y - cStartNv.y * w);
        PointF centerPmoveEndValt = new PointF(centerP.x - cEndNv.x * w, centerP.y - cEndNv.y * w);

        detectPath.rewind();

        switch (mode) {
            case ARC:{
                detectPath.moveTo(startPmoveV.x, startPmoveV.y);
                detectPath.addArc(centerP.x - radius, centerP.y - radius, centerP.x + radius, centerP.y + radius,
                        getStartDegree(startP.x, startP.y, centerP.x, centerP.y), degree);
                detectPath.lineTo(endPmoveV.x, endPmoveV.y);
                detectPath.lineTo(endPmoveValt.x, endPmoveValt.y);
                detectPath.addArc(centerP.x - interRadius, centerP.y - interRadius, centerP.x + interRadius, centerP.y + interRadius,
                        getStartDegree(endP.x, endP.y, centerP.x, centerP.y), -degree);
                detectPath.lineTo(startPmoveValt.x, startPmoveValt.y);
                detectPath.lineTo(startPmoveV.x, startPmoveV.y);

                detectPath.addCircle(startP.x, startP.y, w, Path.Direction.CCW);
                detectPath.addCircle(endP.x, endP.y, w, Path.Direction.CCW);
                break;
            }
            case ANGLE:{
                detectPath.moveTo(startPmoveNv.x, startPmoveNv.y);
                detectPath.lineTo(centerPmoveStartV.x, centerPmoveStartV.y);
                detectPath.lineTo(centerPmoveEndV.x, centerPmoveEndV.y);
                detectPath.lineTo(endPmoveNv.x, endPmoveNv.y);
                detectPath.lineTo(endPmoveNvalt.x, endPmoveNvalt.y);
                detectPath.lineTo(centerPmoveEndValt.x, centerPmoveEndValt.y);
                detectPath.lineTo(centerPmoveStartValt.x, centerPmoveStartValt.y);
                detectPath.lineTo(startPmoveNvalt.x, startPmoveNvalt.y);
                detectPath.lineTo(startPmoveNv.x, startPmoveNv.y);

                detectPath.addCircle(startP.x, startP.y, w, Path.Direction.CW);
                detectPath.addCircle(endP.x, endP.y, w, Path.Direction.CW);
                detectPath.addCircle(centerP.x, centerP.y, w, Path.Direction.CW);
                break;
            }
            case LINE:{
                detectPath.moveTo(startPmoveV.x, startPmoveV.y);
                detectPath.addArc(centerP.x - radius, centerP.y - radius, centerP.x + radius, centerP.y + radius,
                        getStartDegree(startP.x, startP.y, centerP.x, centerP.y), degree);
                detectPath.lineTo(endPmoveV.x, endPmoveV.y);
                detectPath.lineTo(endPmoveNv.x, endPmoveNv.y);
                detectPath.lineTo(centerPmoveEndV.x, centerPmoveEndV.y);
                detectPath.lineTo(centerPmoveStartV.x, centerPmoveStartV.y);//5
                detectPath.lineTo(startPmoveNv.x, startPmoveNv.y);//6
                detectPath.lineTo(startPmoveNvalt.x, startPmoveNvalt.y);//7
                detectPath.lineTo(centerPmoveStartValt.x, centerPmoveStartValt.y);//8
                detectPath.lineTo(centerPmoveEndValt.x, centerPmoveEndValt.y);//9
                detectPath.lineTo(endPmoveNvalt.x, endPmoveNvalt.y);
                detectPath.lineTo(endPmoveValt.x, endPmoveValt.y);
                detectPath.addArc(centerP.x - interRadius, centerP.y - interRadius, centerP.x + interRadius, centerP.y + interRadius,
                        getStartDegree(endP.x, endP.y, centerP.x, centerP.y), -degree);
                detectPath.lineTo(startPmoveValt.x, startPmoveValt.y);
                detectPath.lineTo(startPmoveV.x, startPmoveV.y);
                detectPath.addCircle(startP.x, startP.y, w, Path.Direction.CCW);
                detectPath.addCircle(endP.x, endP.y, w, Path.Direction.CCW);
                detectPath.addCircle(centerP.x, centerP.y, w, Path.Direction.CCW);
                break;
            }
            case SOLID:{
                detectPath.moveTo(centerPmoveStartV.x, centerPmoveStartV.y);
                detectPath.lineTo(startPmoveNv.x, startPmoveNv.y);
                detectPath.lineTo(startPmoveV.x, startPmoveV.y);
                detectPath.addArc(centerP.x - radius, centerP.y - radius, centerP.x + radius, centerP.y + radius,
                        getStartDegree(startP.x, startP.y, centerP.x, centerP.y), degree);
                detectPath.lineTo(endPmoveV.x, endPmoveV.y);
                detectPath.lineTo(endPmoveNv.x, endPmoveNv.y);
                detectPath.lineTo(centerPmoveEndV.x, centerPmoveEndV.y);
                detectPath.lineTo(centerPmoveStartV.x, centerPmoveStartV.y);
                detectPath.addCircle(startP.x, startP.y, w, Path.Direction.CCW);
                detectPath.addCircle(endP.x, endP.y, w, Path.Direction.CCW);
                detectPath.addCircle(centerP.x, centerP.y, w, Path.Direction.CCW);
                break;
            }
        }
    }

    private float getStartDegree(float x, float y, float cx, float cy) {
        PointF v1 = new PointF(1, 0);
        PointF v2 = new PointF(x - cx, y - cy);
        float arc1 = (float) Math.atan2(v1.x, v1.y);
        float arc2 = (float) Math.atan2(v2.x, v2.y);
        return (float) Math.toDegrees(arc1 - arc2);
    }

    private void updateTextPosition() {
        Matrix matrix = new Matrix();
        float degree = protractorDegree / 2f;

        text = String.format(Locale.ENGLISH, "%.1f°", Math.abs(protractorDegree));
        float tw = textPaint.measureText(text);
        float th = TextLayoutUtil.getHeightOfString(textPaint, text);

        matrix.setRotate(degree, centerP.x, centerP.y);
        float[] points = new float[]{startP.x, startP.y};
        matrix.mapPoints(points);
        PointF middleV = getUnitVector(points[0] - centerP.x, points[1] - centerP.y);
        textP = new PointF();
        textP.x = centerP.x + middleV.x * textRadius;
        textP.y = centerP.y + middleV.y * textRadius;
        textP.x = centerP.x + middleV.x * textRadius;
        textP.y = centerP.y + middleV.y * textRadius;

        float w = tw / 2f + textBackgroundHorizontalPadding / 2f + measurementRectRadius / 2f;
        float h = th / 2f + textBackgroundVerticalPadding / 2f + measurementRectRadius / 2f;
        textRectF.set(textP.x - w, textP.y - h, textP.x + w, textP.y + h);

        textP.x -= tw / 2f;
        textP.y += th / 2f;
    }

    private PointF getUnitVector(float u, float v) {
        PointF unitV = new PointF(u, v);
        float d = unitV.length();
        unitV.x /= d;
        unitV.y /= d;

        return unitV;
    }

    @Override
    public void setColor(int color) {
        super.setColor(color);
        protractorPaint.setColor(color);
    }

    public void setCenterP(float x, float y) {
        if (centerP.x != x || centerP.y != y) {
            centerP.x = x;
            centerP.y = y;
            update = true;
        }
    }

    public void setStartP(float x, float y) {
        if (startP.x != x || startP.y != y) {
            startP.x = x;
            startP.y = y;
            update = true;
        }
    }

    public void setProtractorDegree(float d) {
        if (protractorDegree != d) {
            protractorDegree = d;
            update = true;
        }
    }

    @Override
    public void moveBy(float dx, float dy) {
        super.moveBy(dx, dy);
        centerP.x += dx;
        centerP.y += dy;
        startP.x += dx;
        startP.y += dy;

        update();
    }

    @Override
    public void scaleFromPoint(float rateX, float rateY, PointF p) {
        Matrix matrix = new Matrix();
        float rate = rateX != 1.0f ? rateX : rateY;
        matrix.setScale(rate, rate, p.x, p.y);
        float[] points = new float[]{startP.x, startP.y, centerP.x, centerP.y};
        matrix.mapPoints(points);
        startP.x = points[0];
        startP.y = points[1];
        centerP.x = points[2];
        centerP.y = points[3];
        protractorPaint.setStrokeWidth(protractorPaint.getStrokeWidth() * rate);

        update();
    }

    @Override
    public void rotateFromPoint(float degree, PointF p) {
        super.rotateFromPoint(degree, p);
        Matrix matrix = new Matrix();
        matrix.setRotate(degree, p.x, p.y);
        float[] points = new float[]{centerP.x, centerP.y, startP.x, startP.y};
        matrix.mapPoints(points);
        centerP.x = points[0];
        centerP.y = points[1];
        startP.x = points[2];
        startP.y = points[3];

        update();
    }

    private void updateBounds() {
        float offset = padding;
        RectF rectF = new RectF();
        detectPath.computeBounds(rectF, true);
        x = rectF.left - offset;
        y = rectF.top - offset;
        width = rectF.width() + 2 * offset;
        height = rectF.height() + 2 * offset;

        setBounds(x, y, width, height);
    }

    @Override
    public void getRegion(Region out) {
        RectF rectF = new RectF();
        Rect rect = new Rect();
        Region clipRegion = new Region();
        detectPath.computeBounds(rectF, true);
        rectF.roundOut(rect);
        clipRegion.set(rect);
        detectRegion.setPath(detectPath, clipRegion);
        out.set(detectRegion);
    }

    @Override
    public BaseItem copy() {
        ProtractorItem protractorItem = new ProtractorItem(context, protractorPaint.getColor(), protractorPaint.getStrokeWidth(),
                new PointF(centerP.x, centerP.y), new PointF(startP.x, startP.y), protractorDegree, mode);
        protractorItem.setShowDegree(showDegree);
        return protractorItem;
    }

    public static ProtractorItem copyFromStateFile(Context context, @ColorInt int strokeColor, float strokeWidth,
                                                                                            PointF centerP, PointF startP, float protractorDegree, ProtractorPaintMode mode, boolean showDegree) {
        ProtractorItem protractorItem = new ProtractorItem(context, strokeColor, strokeWidth, centerP, startP, protractorDegree, mode);
        protractorItem.setShowDegree(showDegree);
        return protractorItem;
    }

    @Override
    public BaseStateFile getStateFile() {
        return new ProtractorItemStateFile(this, protractorPaint.getColor(), protractorPaint.getStrokeWidth(),
                centerP, startP, protractorDegree, mode, showDegree);
    }

    @Override
    public int getColorToBeCompared() {
        @ColorInt int color = super.getColor();
        if (color == hlBlack) {
            color = black;
        } else if (color == hlWhite) {
            color = white;
        } else if (color == hlRed) {
            color = red;
        } else if (color == hlYellow) {
            color = yellow;
        } else if (color == hlGreen) {
            color = green;
        } else if (color == hlBlue) {
            color = blue;
        } else if (color == hlPurple) {
            color = purple;
        }

        return color;
    }

    private void update() {
        updatePath();
        updateDetectPath();
        updateBounds();
        updateTextPosition();
    }

    @Override
    public void render(Canvas canvas, Paint paint, Context context) {
        if (update) {
            update();
            update = false;
        }
        canvas.drawPath(protractorPath, protractorPaint);
        if (showDegree) {
            canvas.drawRoundRect(textRectF, measurementRectRadius, measurementRectRadius, textRectFPaint);
            canvas.drawText(text, textP.x, textP.y, textPaint);
        }
    }

    private void log(String s) {
        Log.d(ProtractorItem.class.getSimpleName(), s);
    }

    public void setShowDegree(boolean showDegree) {
        this.showDegree = showDegree;
    }
}
